﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Resources;
using EdiNet.Properties;
using System.Globalization;
using System.Diagnostics;

namespace EdiNet
{
    public class EdiStreamReader : IDisposable
    {

        const int DEFAULT_BUFFER_SIZE = 4096;
        readonly bool IgnoreSpacesAndCrLfAfterSegmentTerminator = true;

        Stream baseStream;
        Encoding encoding;
        byte[] buffer;
        int bufferOffset;
        long streamOffset;
        int actualBufferSize;

        enum ReaderState
        {
            /// <summary>
            /// The Read method has not been called or IEA segment has been read.
            /// </summary>
            Initial,
            /// <summary>
            /// The Read method has been called. Additional methods may be called on the  reader.
            /// </summary>
            Interactive,
            /// <summary>
            /// The end of the file has been reached successfully.
            /// </summary>
            EndOfFile,
        }


        ReaderState readerState = ReaderState.Initial;
        EdiReaderWriterSettings settings;

        public EdiStreamReader(Stream ediStream)
            : this(ediStream, DefaultEdiEncoding.Encoding)
        {
        }
        public EdiStreamReader(Stream ediStream, Encoding enc)
            : this(ediStream, enc, DEFAULT_BUFFER_SIZE)
        {
        }

        public EdiStreamReader(Stream ediStream, Encoding enc, int bufferSize)
        {
            if (null == ediStream)
                throw new ArgumentNullException("ediStream");

            if (!enc.IsSingleByte)
                throw new NotImplementedException(Resources.E_MULTI_BYTE_ENCODING_NOT_SUPPORTED);

            this.baseStream = ediStream;
            this.encoding = enc;
            buffer = new byte[bufferSize];
        }

        /// <summary>
        /// EdiReaderWriterSettings for the current envelope. null - if there is no envelope
        /// </summary>
        public EdiReaderWriterSettings CurrentEdiReaderWriterSettings
        {
            get
            {
                return settings;
            }
        }

        /// <summary>
        /// Returns position within the current stream. After a ReadSegment call Position is the starting position of the next segment.
        /// EdiStreamReader maintains an internal buffer, Position returned by this property is usually different from the base stream position.
        /// </summary>
        public long Position
        {
            get
            {
                return streamOffset + bufferOffset;
            }
        }

        /// <summary>
        /// Reads the next node from the stream.
        /// </summary>
        /// <returns>
        ///  DataSegment if the next segment was read successfully; null if there are no more data to read.
        /// </returns>
        public DataSegment ReadSegment()
        {
            if (readerState == ReaderState.Initial)
            {
                GuessEDIStandardAndReaderSettings();
                if (readerState == ReaderState.EndOfFile)
                    return null;
            }

            DataSegment res = ReadSegmentImpl();

            if (readerState != ReaderState.EndOfFile)
            {
                if ((settings.Standard == EdiStandard.X12 && res.SegId == "IEA")
                    || (settings.Standard == EdiStandard.EDIFACT && res.SegId == "UNZ")
                    )
                    readerState = ReaderState.Initial;
            }
            return res;
        }

        /// <summary>
        /// Sets position within the stream. If position is outside of the current envelope or if there is no current envelope behaviour is undefined.
        /// </summary>
        /// <param name="position"></param>
        /// <param name="location"></param>
        public void Seek(long position)
        {
            baseStream.Seek(position, SeekOrigin.Begin);
            readerState = ReaderState.Interactive;
            bufferOffset = actualBufferSize = 0;
            streamOffset = position;
        }

        /// <summary>
        /// Closes underlying stream
        /// </summary>
        public void Close()
        {
            baseStream.Close();
            readerState = ReaderState.EndOfFile;
        }

        /// <summary>
        /// Closes underlying stream
        /// </summary>
        public void Dispose()
        {
            Close();
        }


        /// <summary>
        /// Does the actual parsing
        /// </summary>
        /// <returns>
        ///  DataSegment if the next segment was read successfully; null if there are no more data to read.
        /// </returns>
        private DataSegment ReadSegmentImpl()
        {
            DataSegment seg = null;
            DataElement el = null;
            int elStart = bufferOffset;
            string currentElement = string.Empty;
            for (; ; ++bufferOffset)
            {
                if (bufferOffset >= actualBufferSize)
                {
                    if (elStart < bufferOffset)
                        currentElement += encoding.GetString(buffer, elStart, actualBufferSize - elStart);
                    if (!ReadBlock())
                    {
                        if (!string.IsNullOrEmpty(currentElement))
                        {
                            if (null != el)
                                el.Add(currentElement);
                            else if (null != seg)
                                seg.Add(currentElement);
                        }
                        break;
                    }
                    elStart = bufferOffset;
                }
                byte b = buffer[bufferOffset];
                if (settings.IsSeparator(b))
                {
                    currentElement += encoding.GetString(buffer, elStart, bufferOffset - elStart);
                    if (null == seg)
                    {
                        seg = new DataSegment(currentElement);
                        if (settings.Standard == EdiStandard.X12 && currentElement == "ISA")
                        {
                            ++bufferOffset;
                            ReadX12IsaSegmentImpl(seg);
                            return seg;
                        }
                    }
                    else
                    {

                        if (null != el)
                            el.Add(currentElement);
                        else if (null != seg)
                            el = seg.Add(currentElement);

                        if (b != settings.ComponentSeparator)
                            el = null;
                    }
                    currentElement = string.Empty;

                    elStart = bufferOffset + 1;

                    if (b == settings.SegmentTerminator)
                    {
                        ++bufferOffset;
                        if (IgnoreSpacesAndCrLfAfterSegmentTerminator && ReadBlock())
                            SkipSpaces();
                        break;
                    }
                }

            }
            return seg;
        }

        /// <summary>
        /// Does the actual parsing for X12 ISA seg, ignores all separators except the element separator
        /// </summary>
        /// <returns>
        ///  DataSegment if the next segment was read successfully; null if there are no more data to read.
        /// </returns>
        private void ReadX12IsaSegmentImpl(DataSegment seg)
        {
            int elStart = bufferOffset;
            string currentElement = string.Empty;
            for (; ; ++bufferOffset)
            {
                if (bufferOffset >= actualBufferSize)
                {
                    if (elStart < bufferOffset)
                        currentElement += encoding.GetString(buffer, elStart, actualBufferSize - elStart);
                    if (!ReadBlock())
                    {
                        if (!string.IsNullOrEmpty(currentElement))
                        {
                            seg.Add(currentElement);
                        }
                        break;
                    }
                    elStart = bufferOffset;
                }
                byte b = buffer[bufferOffset];
                if (b == settings.DataElementSeparator || b == settings.SegmentTerminator)
                {
                    currentElement += encoding.GetString(buffer, elStart, bufferOffset - elStart);
                    seg.Add(currentElement);
                    currentElement = string.Empty;

                    elStart = bufferOffset + 1;

                    if (b == settings.SegmentTerminator)
                    {
                        ++bufferOffset;
                        if (IgnoreSpacesAndCrLfAfterSegmentTerminator && ReadBlock())
                            SkipSpaces();
                        break;
                    }
                }

            }
        }

        /// <summary>
        /// Skip space characters
        /// </summary>
        private void SkipSpaces()
        {
            for (; ; ++bufferOffset)
            {
                if (bufferOffset >= actualBufferSize)
                {
                    if (!ReadBlock())
                        break;
                }
                if (!IsSpace(buffer[bufferOffset]))
                    break;
            }
        }


        /// <summary>
        /// Reads bytes from the stream but does not advance stream position. Used to simplify ISA\UNA parsing on smaller buffers. 
        /// And smaller buffers are only intended for unit tests to test all kinds of boundary conditions.
        /// </summary>
        /// <param name="n">Number of bytes to peek</param>
        /// <returns>
        /// If the internal buffer position = 0 and the size of the internall buffer is greater or equal to n - returns a reference to the internal buffer (buffer may contain more characters than requested)
        /// Otherwise a new byte array with Max(Stream.Length, n) bytes 
        /// </returns>
        private byte[] Peek(int n)
        {
            ReadBlock();

            if (actualBufferSize >= n && bufferOffset == 0)
                return buffer;

            long savedPos = Position;

            byte[] _buf = new byte[n];
            int nRead = 0;
            while (nRead < n)
            {
                if (bufferOffset >= actualBufferSize)
                {
                    if (!ReadBlock())
                        break;
                }
                int toCopy = Math.Min(actualBufferSize - bufferOffset, n - nRead);
                Buffer.BlockCopy(buffer, bufferOffset, _buf, nRead, toCopy);
                bufferOffset += toCopy;
                nRead += toCopy;
            }
            Seek(savedPos);
            if (nRead != n)
            {
                byte[] _smallerBuf = new byte[nRead];
                Buffer.BlockCopy(_buf, 0, _smallerBuf, 0, nRead);
                return _smallerBuf;
            }
            return _buf;
        }

        /// <summary>
        /// Uses internal list of space characters
        /// </summary>
        /// <param name="b"></param>
        /// <returns></returns>
        private bool IsSpace(byte b)
        {
            //hardcoded for now, do something with encoding and conversion later
            //        static readonly byte[] SPACE_CHARACTERS = new byte[] { 0x04, 0x09, 0x0A, 0x0D, 0x1A, 0x20, };
            //          return Array.BinarySearch<byte>(SPACE_CHARACTERS, b) >= 0;

            return b == 0x04 || b == 0x09 || b == 0x0A || b == 0x0D || b == 0x1A || b == 0x20;
        }

        /// <summary>
        /// Determines EDI standard and reader settings by parsing first 110 bytes of the stream
        /// </summary>
        private void GuessEDIStandardAndReaderSettings()
        {
            SkipSpaces();
            if (readerState == ReaderState.EndOfFile)
                return;
            byte[] _buf = Peek(110);

            EdiStandard standard = GuessStandard(encoding, _buf);
            switch (standard)
            {
                case EdiStandard.X12:
                    GetReaderSettingsFromIsa(encoding, _buf);
                    break;
                case EdiStandard.EDIFACT:
                    GetEdifactReaderSettings(encoding, _buf);
                    break;
                default:
                    throw new NotImplementedException(standard.ToString());
            }
            readerState = ReaderState.Interactive;
        }



        /// <summary>
        /// Reads next chunk of data from stream into the internal buffer. If the internal buffer is not empty - no actual read performed. Sets state to EOF if enod of file is reached
        /// </summary>
        /// <returns></returns>
        private bool ReadBlock()
        {
            if (bufferOffset < actualBufferSize)
                return true;
            if (readerState == ReaderState.EndOfFile)
                return false;
            streamOffset += actualBufferSize;
            bufferOffset = 0;

            actualBufferSize = baseStream.Read(buffer, 0, buffer.Length);
            bool res = actualBufferSize > 0;
            if (!res)
                readerState = ReaderState.EndOfFile;
            return res;
        }


        /// <summary>
        /// Guess EDI standard, uses first 3 bytes from the provided buffer
        /// </summary>
        /// <param name="encoding"></param>
        /// <param name="_buf"></param>
        /// <returns></returns>
        private EdiStandard GuessStandard(Encoding encoding, byte[] _buf)
        {
            Debug.Assert(_buf.Length >= 3);
            string segId = encoding.GetString(_buf, 0, 3);

            switch (segId)
            {
                case "ISA":
                    return EdiStandard.X12;
                case "UNA":
                case "UNB":
                    return EdiStandard.EDIFACT;
                default:
                    throw new EdiParseException(Resources.E_PARSE_UNRECOGNIZED_FILE_FORMAT);
            }
        }


        /// <summary>
        /// Reads EDI settings (separators) from ISA segment. Element separator is the 4th character, etc...
        /// </summary>
        /// <param name="encoding"></param>
        /// <param name="_buf"></param>
        /// <returns></returns>
        private void GetReaderSettingsFromIsa(Encoding encoding, byte[] _buf)
        {
            if (_buf.Length < 108)
                throw new EdiParseException(Resources.E_PARSE_ISA_ERROR);

            byte elementSeparator = _buf[3];
            byte repetitionSeparator = _buf[82];
            byte componentSeparator = _buf[104];
            byte segmentTerminator = _buf[105];
            int segmentTerminatorPosition = 105;
            //hypothetically ICN can be negative
            if (_buf[90] == '-')
            {
                componentSeparator = _buf[105];
                segmentTerminator = _buf[106];
                segmentTerminatorPosition = 106;
            }
            SegmentTerminatorSuffix suffix = GetSegmentTerminatorSuffix(_buf, segmentTerminatorPosition);
            this.settings = new EdiReaderWriterSettings(encoding, elementSeparator, repetitionSeparator, componentSeparator, segmentTerminator,
                0, suffix, EdiStandard.X12);
        }
        private static SegmentTerminatorSuffix GetSegmentTerminatorSuffix(byte[] _buf, int segmentTerminatorPosition)
        {
            SegmentTerminatorSuffix suffix = SegmentTerminatorSuffix.None;
            byte suffix1 = _buf[segmentTerminatorPosition + 1];
            byte suffix2 = _buf[segmentTerminatorPosition + 2];
            if (suffix1 == '\r')
            {
                if (suffix2 == '\n')
                    suffix = SegmentTerminatorSuffix.CRLF;
                else
                    suffix = SegmentTerminatorSuffix.CR;
            }
            else if (suffix1 == '\n')
            {
                suffix = SegmentTerminatorSuffix.LF;
            }
            return suffix;
        }

        private void GetEdifactReaderSettings(Encoding encoding, byte[] _buf)
        {
            Debug.Assert(_buf.Length >= 3);
            string segId = encoding.GetString(_buf, 0, 3);
            switch (segId)
            {
                case "UNA":
                    GetEdifactReaderSettingsFromUna(encoding, _buf);
                    break;
                case "UNB":
                    this.settings = EdiReaderWriterSettings.DefaultEdifact;
                    break;
                default:
                    throw new EdiParseException(Resources.E_PARSE_UNRECOGNIZED_FILE_FORMAT);
            }
        }


        private void GetEdifactReaderSettingsFromUna(Encoding encoding, byte[] _buf)
        {
            if (_buf.Length < 8)
                throw new EdiParseException(Resources.E_PARSE_UNA_ERROR);

            byte componentDataElementSeparator = _buf[3];
            byte dataElementSeparator = _buf[4];
            //byte decimalNotation = _buf[5]; //decimal mark override is not implemented
            byte releaseIndicator = _buf[6];
            //byte reserver = _buf[7];
            byte segmentTerminator = _buf[8];
            SegmentTerminatorSuffix suffix = GetSegmentTerminatorSuffix(_buf, 8);
            this.settings = new EdiReaderWriterSettings(encoding, dataElementSeparator, 0, componentDataElementSeparator, segmentTerminator, releaseIndicator, suffix, EdiStandard.EDIFACT);

            ReadSegmentImpl(); //skip UNA
        }
    }
}
